/**
 * FileSystem.java
 * Copyright (C) 2010 Yuanyan<yuanyan.cao@gmail.com>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * For information on how to redistribute this software under
 * the terms of a license other than GNU General Public License
 * contact yuanyan.cao@gmail.com
 */
package edu.rabbit.kernel.fs;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.security.SecureRandom;
import java.util.EnumSet;
import java.util.Set;


import edu.rabbit.DbErrorCode;
import edu.rabbit.DbException;
import edu.rabbit.IOErrorCode;
import edu.rabbit.DbIOException;

/**
 * Default implementation of IFileSystem.
 * 
 * 
 * @author Yuanyan<yanyan.cao@gmail.com>
 * 
 * 
 */
public class DbFileSystem implements IFileSystem {

    public static final String FS_NAME = DbFileSystem.class.getCanonicalName();

    /**
     * Temporary files are named starting with this prefix followed by random
     * alphanumeric characters, and no file extension. They are stored in the
     * OS's standard temporary file directory, and are deleted prior to exit.
     * 
     * If  is being embedded in another program, you may wish to change
     * the prefix to reflect your program's name, so that if your program exits
     * prematurely, old temporary files can be easily identified.
     * 
     * TODO - specify a way to change DB_TEMP_FILE_PREFIX ( e.g. may be
     * system property name as DB_TEMP_FILE_PREFIX ) .
     * 
     * ********************************************************************
     * 
     * Below some from SQLite comments:
     * 
     * 2006-10-31: The default prefix used to be "sqlite_". But then Mcafee
     * started using SQLite in their anti-virus product and it started putting
     * files with the "sqlite" name in the c:/temp folder. This annoyed many
     * windows users. Those users would then do a Google search for "sqlite",
     * find the telephone numbers of the developers and call to wake them up at
     * night and complain. For this reason, the default name prefix is changed
     * to be "sqlite" spelled backwards. So the temp files are still identified,
     * but anybody smart enough to figure out the code is also likely smart
     * enough to know that calling the developer will not help get rid of the
     * file.
     */

    private static final String DB_TEMP_FILE_PREFIX = "tejlqs_";

    private final SecureRandom random = new SecureRandom();

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.rabbit.core.IFileSystem#getName()
     */
    public String getName() {
        return FS_NAME;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.rabbit.core.IFileSystem#open(java.io.File,
     * java.util.Set)
     */
    public IFile open(final File path, final FileType type, final Set<FileOpenPermission> permissions)
            throws DbException {

        if (null == type)
            throw new DbException(DbErrorCode.BAD_PARAMETER, "File type must not be null to open file");

        if (null == permissions || permissions.isEmpty())
            throw new DbException(DbErrorCode.BAD_PARAMETER,
                    "Permissions must not be null or empty to open file");

        boolean isExclusive = permissions.contains(FileOpenPermission.EXCLUSIVE);
        boolean isDelete = permissions.contains(FileOpenPermission.DELETEONCLOSE);
        boolean isCreate = permissions.contains(FileOpenPermission.CREATE);
        boolean isReadonly = permissions.contains(FileOpenPermission.READONLY);
        boolean isReadWrite = permissions.contains(FileOpenPermission.READWRITE);

        /*
         * Check the following statements are true:
         * 
         * (a) Exactly one of the READWRITE and READONLY flags must be set, and
         * (b) if CREATE is set, then READWRITE must also be set, and (c) if
         * EXCLUSIVE is set, then CREATE must also be set. (d) if DELETEONCLOSE
         * is set, then CREATE must also be set.
         */
        assert ((!isReadonly || !isReadWrite) && (isReadWrite || isReadonly));
        assert (!isCreate || isReadWrite);
        assert (!isExclusive || isCreate);
        assert (!isDelete || isCreate);

        /*
         * The main DB, main journal, and master journal are never automatically
         * deleted
         */

        assert (FileType.MAIN_DB != type || !isDelete);
        assert (FileType.MAIN_JOURNAL != type || !isDelete);
        assert (FileType.MASTER_JOURNAL != type || !isDelete);

        final File filePath;

        if (null != path) {
            try {
                filePath = path.getCanonicalFile();
            } catch (IOException e) {
                // TODO may through exception for missing file.
                throw new DbException(DbErrorCode.CANTOPEN, e);
            }
        } else {

            assert (isDelete && !(isCreate && (FileType.MASTER_JOURNAL == type || FileType.MAIN_JOURNAL == type)));
            try {
                filePath = getTempFile();
            } catch (IOException e) {
                throw new DbException(DbErrorCode.CANTOPEN, e);
            }

            assert (null != filePath);
        }

        if (!isReadWrite && !(filePath.isFile() && filePath.canRead())) {
            throw new DbException(DbErrorCode.CANTOPEN);
        }

        String mode = "rw";
        if (isReadonly && !isReadWrite && !isCreate && !isExclusive) {
            mode = "r";
        } else if (isReadWrite && !isExclusive && filePath.isFile() && !filePath.canWrite() && filePath.canRead()) {
            // force opening as read only.
            Set<FileOpenPermission> ro = EnumSet.copyOf(permissions);
            ro.remove(FileOpenPermission.READWRITE);
            ro.remove(FileOpenPermission.CREATE);
            ro.add(FileOpenPermission.READONLY);

            return open(filePath, type, ro);
        }

        RandomAccessFile file = null;
        try {
            file = FileUtil.openFile(filePath, mode);
        } catch (FileNotFoundException e) {

            if (isReadWrite && !isExclusive) {
                /* Failed to open the file for read/write access. Try read-only. */
                Set<FileOpenPermission> ro = EnumSet.copyOf(permissions);
                ro.remove(FileOpenPermission.READWRITE);
                ro.remove(FileOpenPermission.CREATE);
                ro.add(FileOpenPermission.READONLY);
                return open(filePath, type, ro);
            }

            throw new DbException(DbErrorCode.CANTOPEN);
        }

        boolean noLock = FileType.MAIN_DB != type;

        return new DbFile(this, file, filePath, type, permissions, noLock);

    }

    /**
     * @return
     * @throws IOException
     */
    public File getTempFile() throws IOException {
        return File.createTempFile(DB_TEMP_FILE_PREFIX, null);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.rabbit.core.IFileSystem#delete(java.io.File,
     * boolean)
     */
    public boolean delete(File path, boolean sync) {
        assert (null != path);
        return FileUtil.deleteFile(path, sync);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.rabbit.core.IFileSystem#access(java.io.File,
     * org.tmatesoft.rabbit.core.FileAccesPermission)
     */
    public boolean access(File path, FileAccesPermission permission) throws DbException {

        assert (null != path);
        assert (null != permission);

        switch (permission) {
        case EXISTS:
            return path.exists();

        case READONLY:
            return path.canRead() && !path.canWrite();

        case READWRITE:
            return path.canRead() && path.canWrite();

        default:
            throw new DbException(DbErrorCode.INTERNAL, "Unhandled FileAccesPermission value :"
                    + permission.name());
        }

    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.rabbit.core.IFileSystem#currentTime()
     */
    public long currentTime() {
        return System.currentTimeMillis();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.rabbit.core.IFileSystem#randomness(int)
     */
    public byte[] randomness(int numBytes) {
        assert (numBytes > 0);
        final byte[] bytes = new byte[numBytes];
        random.nextBytes(bytes);
        return bytes;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.rabbit.core.IFileSystem#sleep(int)
     */
    public long sleep(long microseconds) {
        assert (microseconds > 0);
        final long t = System.currentTimeMillis();
        try {
            Thread.sleep(microseconds);
        } catch (InterruptedException e) {
        }
        return System.currentTimeMillis() - t;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.tmatesoft.rabbit.core.IFileSystem#memJournalOpen()
     */
    public IFile memJournalOpen() {
        return new MemoryJournal();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.tmatesoft.rabbit.core.IFileSystem#getFullPath(java.io.File)
     */
    public String getFullPath(File filename) throws DbException {
        assert (filename != null);
        try {
            return filename.getCanonicalPath();
        } catch (IOException e) {
            throw new DbIOException(IOErrorCode.IOERR_ACCESS, e);
        }
    }

}
